---
sidebar_position: 9
title: 分页与方言
description: 使用 dbVisitor ORM 的分页查询能力查询数据库数据。
---
# 分页与方言

## 分页查询

dbVisitor 内置了分页查询机制，使用方便且无需任何配置。具体工作方式为：

- 框架会先生成一条未经改写的最终执行 SQL 及参数，并通过 `BoundSql` 类型承载。
- 然后会根据 `PageSqlDialect` 接口将原始 SQL 处理成分页 SQL。结果仍然是 `BoundSql` 类型承载。
- 执行最终的 SQL 完成分页查询

### LambdaTemplate 方式

使用 `LambdaTemplate` 进行分页查询

```java
// 构造 LambdaTemplate 和初始化一些数据
DataSource dataSource = DsUtils.dsMySql();
LambdaTemplate lambdaTemplate = new LambdaTemplate(dataSource);
lambdaTemplate.loadSQL("CreateDB.sql");

// 构建分页对象，每页 3 条数据(默认第一页的页码为 0)
Page pageInfo = new PageObject();
pageInfo.setPageSize(3);

// 分页查询数据
List<TestUser> pageData1 = lambdaTemplate.lambdaQuery(TestUser.class)
    .usePage(pageInfo)
    .queryForList();

// 分页查询下一页数据
pageInfo.nextPage();
List<TestUser> pageData2 = lambdaTemplate.lambdaQuery(TestUser.class)
    .usePage(pageInfo)
    .queryForList();
```

### DalSession 方式

以一个简单SQL语句为例，演示 mapper 层面支持分页查询：

```xml
<select id="queryListByAge">
    select * from `test_user` where age = #{age}
</select>
```

Mapper 无需任何修改，若使用 `DalSession` 方式执行分页查询。需要构建分页对象然后传递给 `queryStatement` 方法。

```java
Map<String, Object> ages = new HashMap<>();
ages.put("age", 26);

PageObject page = new PageObject(); // 分页查询对象
page.setPageSize(20);
List<Object> result = dalSession.queryStatement("queryListByAge", ages, page);
```

### Mapper 接口方式

首先将 Mapper `queryListByAge` 方法映射到接口上，然后在接口方法中增加 Page 分页查询参数对象。例如：

```java
@RefMapper("...")
public interface PageExecuteDal {
    List<TestUser> queryListByAge(@Param("age") int age, Page pageInfo);
}
```

或者如下。区别于上面的是可以选择返回一个分页查询结果 `PageResult`，分页结果对象中包含了分页查询信息和总数数据。

:::tip
使用分页查询结果对象会产生额外的一条 `count` 查询。
:::

```java
@RefMapper("...")
public interface PageExecuteDal {
    PageResult<TestUser> queryListByAge(@Param("name") String name, Page pageInfo);
}
```

### 分页对象

dbVisitor 提供了一个分页查询工具类 `PageObject`, 它实现了 `Page` 接口。并提供了如下一些工具属性/方法。

export const HighAttr = () => (
    <span style={{ backgroundColor: 'rgb(227 153 17)', color: '#fff', borderRadius: '2px', padding: '0.2rem'}}>属性</span>
);


export const HighMethod = () => (
    <span style={{ backgroundColor: 'rgb(17 179 227)', color: '#fff', borderRadius: '2px', padding: '0.2rem'}}>方法</span>
);

| 名称                                               | 描述                                             |
|--------------------------------------------------|------------------------------------------------|
| <HighAttr/> `pageSize`                           | 分页查询的页大小，默认是 `-1` 表示无穷大                        |
| <HighAttr/> `currentPage`                        | 当前页号                                           |
| <HighAttr/> `pageNumberOffset`                   | 页码偏移量（例如：从 `1` 页作为起始页，可以设置为 `1`。否则第一页的页码是 `0`） |
| <HighMethod/> `long getFirstRecordPosition()`    | 获取本页第一个记录的索引位置                                 |
| <HighMethod/> `long getTotalPage()`              | 获取总页数                                          |
| <HighMethod/> `long getTotalCount()`             | 获取记录总数                                         |
| <HighMethod/> `long firstPage()`                 | 移动到第一页                                         |
| <HighMethod/> `void previousPage()`              | 移动到上一页                                         |
| <HighMethod/> `void nextPage()`                  | 移动到下一页                                         |
| <HighMethod/> `void lastPage()`                  | 移动到最后一页                                        |
| <HighMethod/> `Map<String, Object> toPageInfo()` | 获取分页信息                                         |

`toPageInfo` 方法会返回如下一个分页查询 Map。

```js
{
    "enable" : true,    // 是否启用分页
    "pageSize" : 20,    // 页大小
    "totalCount" : 200, // 总记录数
    "totalPage" : 10,   // 页总数
    "currentPage" : 0,  // 当前页码
    "recordPosition" : 0// 第一条记录的起始记录位置
}
```

## 方言

和方言相关的接口一共有 4 个，其中 `SqlDialect` 是所有其它接口都继承的公共接口。

- `SqlDialect` 基础接口，负责管理关键词清单、生成表名、列名
- `ConditionSqlDialect` 负责条件相关的生成，例如：like 语句
- `InsertSqlDialect` 负责高级 `insert` 语句生成，例如处理：**[冲突策略](./crud/conflict.md)**
- `PageSqlDialect` 负责分页语句生成。

:::tip
实现自定义方言最佳的路线是继承 `AbstractDialect` 抽象类。
:::


### 内置分页查询方言实现

dbVisitor 默认会根据 JDBC 的链接字符串自动匹配方言，支持如下数据库：

| 数据库           | 编码           | 方言类名                | JDBC串识别前缀 |
|---------------|--------------|---------------------| ------------- |
| DB2           | `db2`        | `Db2Dialect`        | `jdbc:db2:***` |
| Apache Derby  | `derby`      | `DerbyDialect`      | `jdbc:derby:***`、`jdbc:log4jdbc:derby:***` |
| 达梦            | `dm`         | `DmDialect`         | `jdbc:dm:***` |
| H2            | `H2`         | `H2Dialect`         | `jdbc:h2::***`、`jdbc:log4jdbc:h2:***` |
| Hive          | `hive`       | `HiveDialect`       | `jdbc:hive:***`、`jdbc:hive2:***` |
| HSQL          | `hsql`       | `HSQLDialect`       | `jdbc:hsqldb:***`、`jdbc:log4jdbc:hsqldb:***` |
| Apache Impala | `impala`     | `ImpalaDialect`     | `jdbc:impala:***` |
| IBM Informix  | `informix`   | `InformixDialect`   | `jdbc:informix-sqli:***`、`jdbc:log4jdbc:informix-sqli:***` |
| 人大金仓          | `kingbase`   | `KingbaseDialect`   | `jdbc:kingbase:***` |
| MariaDB       | `mariadb`    | `MariaDBDialect`    | `jdbc:mariadb:***` |
| MYSQL         | `mysql`      | `MySqlDialect`      | `jdbc:mysql:***`、`jdbc:cobar:***`、`jdbc:log4jdbc:mysql:***`|
| Oracle        | `oracle`     | `OracleDialect`     | `jdbc:oracle:***`、`jdbc:log4jdbc:oracle:***` |
| Phoenix       | `phoenix`    | `PhoenixDialect`    | `jdbc:phoenix:***` |
| PostgreSQL    | `postgresql` | `PostgreSqlDialect` | `jdbc:postgresql:***`、`jdbc:log4jdbc:postgresql:***` |
| SQLite        | `sqlite`     | `SqlLiteDialect`    | `jdbc:sqlite:***` |
| SQL Server    | `sqlserver`  | `SqlServerDialect`  | `jdbc:jtds:***`、`jdbc:microsoft:***`、`jdbc:sqlserver:***`、`jdbc:log4jdbc:jtds:***`、`jdbc:log4jdbc:microsoft:***`、`jdbc:log4jdbc:sqlserver:***` |
| 虚谷数据库         | `xugu`       | `XuGuDialect`       | `jdbc:xugu:***` |

### 自定义方言

若想实现 **分页查询方言** 自定义只需要继承 `AbstractDialect` 抽象类，然后额外在实现 `PageSqlDialect` 接口即可。

`PageSqlDialect` 接口有两个方法，分别用于生成改写后的分页查询 SQL、以及计算 count 的 SQL。

```java
public interface PageSqlDialect extends SqlDialect {
    default BoundSql countSql(BoundSql boundSql) {
        return new BoundSql.BoundSqlObj("SELECT COUNT(*) FROM (" + boundSql.getSqlString() + ") as TEMP_T", boundSql.getArgs());
    }

    BoundSql pageSql(BoundSql boundSql, int start, int limit);
}
```

:::tip
若计算总数的 SQL 只是简单的将其放入子查询并且 count 一下，那么只实现 `pageSql` 用于改写分页语句的 SQL 即可。
:::

**使用自己的新分页查询方言**

对于 `LambdaTemplate` 类通过下面方式来设置自己的方言实现类

```java
LambdaTemplate lambdaTemplate = ...
lambdaTemplate.setDialect(new MyDialect());
```

对于 `DalSession` 需要通过构造方法穿传入。

```java
MappingOptions opt = MappingOptions.buildNew().defaultDialect(new MyDialect());
DalRegistry dalRegistry = new DalRegistry(opt);
DataSource dataSource = ...
DalSession dalSession = new DalSession(dataSource, dalRegistry);
```

通过注册方式，然后 dbVisitor 自动使用新分页查询方言。

以 MySQL 为例，MySQL 的链接字符串格式为 `jdbc:mysql:***`

- 首先 dbVisitor 会通过 `connection.getMetaData().getURL()` 方式拿到链接字符串
- 然后根据上面 **内置方言** 表格中的信息匹配到对应的 ``方言编码``
- 最后通过 `SqlDialectRegister.findOrCreate(<方言编码>);` 方法获取到对应的方言对象。

因此只需要按照上面规则将新的方言注册到 `SqlDialectRegister` 上即可。如下：

```java
SqlDialectRegister.registerDialectAlias(JdbcUtils.MYSQL, MyDialect.class);
```